/* @(#)MemoryResidentTreeStateManager.java
** 
** Copyright (C) 2012 by Steven D. Stamps
**
**             Trademarks & Copyrights
** Flywheel Management Science(TM), Flywheel Management Model(TM),
** Flywheel Story Editor(TM) and FlywheelMS(TM) are exclusive trademarks
** of Steven D. Stamps and may only be used freely for the purpose of
** identifying the unforked version of this software.  Subsequent forks
** may not use these trademarks.  All other rights are reserved.
**
** DecKanGL (Decorated Kanban Glyph Language) and TribKn (Tribal Knowledge)
** are also exclusive trademarks of Steven D. Stamps.  These may be used
** freely within the unforked FlywheelMS application and documentation.
** All other rights are reserved.
**
** gConGUI (game Controller Graphical User Interface) is an exclusive
** trademark of Steven D. Stamps.  This trademark may be used freely
** within the unforked FlywheelMS application and documentation.
** All other rights are reserved.
**
** Trademark information is available at
** <http://www.flywheelms.com/trademarks>
**
** Flywheel Management Science(TM) is a copyrighted body of management
** metaphors, governance processes, and leadership techniques that is
** owned by Steven D. Stamps.  These copyrighted materials may be freely
** used, without alteration, by the community (users and developers)
** surrounding this GPL3-licensed software.  Additional copyright
** information is available at <http://www.flywheelms.org/copyrights>
**
**              GPL3 Software License
** This program is free software: you can use it, redistribute it and/or
** modify it under the terms of the GNU General Public License, version 3,
** as published by the Free Software Foundation. This program is distributed
** in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
** even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
** PURPOSE.  See the GNU General Public License for more details. You should
** have received a copy of the GNU General Public License, in a file named
** COPYING, along with this program.  If you cannot find your copy, see
** <http://www.gnu.org/licenses/gpl-3.0.html>.
*/

package com.flywheelms.library.gcg.treeview;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import android.database.DataSetObserver;
import android.util.Log;

import com.flywheelms.library.fmm.node.impl.governable.FiscalYear;
import com.flywheelms.library.fmm.perspective.FmmPerspective;
import com.flywheelms.library.gcg.treeview.exception.NodeAlreadyInTreeException;
import com.flywheelms.library.gcg.treeview.exception.NodeNotInTreeException;
import com.flywheelms.library.gcg.treeview.node.TreeNodeInfo;
import com.flywheelms.library.gcg.treeview.node.TreeNodeMemoryResident;

/*
 * TODO - need to "pull" most of this up into a GCG super class
 */
public class GcgTreeStateMediatorMemoryResident implements GcgTreeStateMediator {
	
    private static final String LOGGING_TAG = GcgTreeStateMediatorMemoryResident.class.getSimpleName();
    private static final long serialVersionUID = 1L;
    private final Map<TreeNodeInfo, TreeNodeMemoryResident> treeNodeMap =
    		new HashMap<TreeNodeInfo, TreeNodeMemoryResident>();
    private final TreeNodeInfo rootTreeNodeInfo =
    		new TreeNodeInfo(new FiscalYear("JUNK"), -1, true, FmmPerspective.STRATEGIC_PLANNING);
    private final TreeNodeMemoryResident rootTreeNodeMMemoryResident = 
    		new TreeNodeMemoryResident(this.rootTreeNodeInfo, null, -1, true);
    private transient List<TreeNodeInfo> visibleTreeNodeInfoList = null;
    private transient List<TreeNodeInfo> unmodifiableVisibleTreeNodeInfoList = null;
    private boolean visibleByDefault = true;
    private final transient Set<DataSetObserver> treeStateObservers = new HashSet<DataSetObserver>();
    private transient GcgTreeFilter treeFilter;
    
    public GcgTreeStateMediatorMemoryResident(GcgTreeFilter aTreeFilter) {
    	super();
    	this.treeNodeMap.put(this.rootTreeNodeInfo, this.rootTreeNodeMMemoryResident);
    	this.treeFilter = aTreeFilter;
    }

    private synchronized void treeStateChanged() {
    	this.visibleTreeNodeInfoList = null;
    	this.unmodifiableVisibleTreeNodeInfoList = null;
        for (final DataSetObserver theTreeObserver : this.treeStateObservers) {
            theTreeObserver.onChanged();
        }
    }

    public void setVisibleByDefault(final boolean bVisibleByDefault) {
        this.visibleByDefault = bVisibleByDefault;
    }

    private TreeNodeMemoryResident getNodeFromTreeMapOrThrow(final TreeNodeInfo aTreeNodeInfo) {
        if (aTreeNodeInfo == null) {
            throw new NodeNotInTreeException("(null)");
        }
        final TreeNodeMemoryResident theTreeNode = this.treeNodeMap.get(aTreeNodeInfo);
        if (theTreeNode == null) {
            throw new NodeNotInTreeException(aTreeNodeInfo.toString());
        }
        return theTreeNode;
    }

    private TreeNodeMemoryResident getNodeFromTreeMapOrRootNode(final TreeNodeInfo aTreeNodeInfo) {
        if (aTreeNodeInfo == null) {
            return this.rootTreeNodeMMemoryResident;
        }
        return getNodeFromTreeMapOrThrow(aTreeNodeInfo);
    }

    private void validateNodeNotInTreeMap(final TreeNodeInfo aTreeNodeInfo) {
        final TreeNodeMemoryResident theTreeNode = this.treeNodeMap.get(aTreeNodeInfo);
        if (theTreeNode != null) {
            throw new NodeAlreadyInTreeException(aTreeNodeInfo.toString(), theTreeNode.toString());
        }
    }

//    @Override
//    public synchronized TreeNodeInfo getTreeNodeInfo(final TreeNodeInfo aTreeNodeInfo) {
//        final MemoryResidentTreeNode theTreeNode = getNodeFromTreeMapOrThrow(aTreeNodeInfo);
//        final List<MemoryResidentTreeNode> theChildTreeNodeList = theTreeNode.getChildTreeNodeList();
//        boolean isExpanded = false;
//        if (!theChildTreeNodeList.isEmpty() && theChildTreeNodeList.get(0).isVisible()) {
//            isExpanded = true;
//        }
//        return new TreeNodeInfo(theTreeNode.getTargetObject(), theTreeNode.getLevel(), !theChildTreeNodeList.isEmpty(),
//                theTreeNode.isVisible(), isExpanded);
//    }

    @Override
    public synchronized List<TreeNodeInfo> getChildren(final TreeNodeInfo aTreeNodeInfo) {
        final TreeNodeMemoryResident theTreeNode = getNodeFromTreeMapOrRootNode(aTreeNodeInfo);
        return theTreeNode.getChildTreeNodeInfoList();
    }

    @Override
    public synchronized TreeNodeInfo getParent(final TreeNodeInfo aTreeNodeInfo) {
        final TreeNodeMemoryResident theTreeNode = getNodeFromTreeMapOrRootNode(aTreeNodeInfo);
        return theTreeNode.getParentTreeNodeInfo();
    }

    private boolean getChildrenVisibility(final TreeNodeMemoryResident aTreeNode) {
        final List<TreeNodeMemoryResident> theChildTreeNodeList = aTreeNode.getChildTreeNodeList();
        if (theChildTreeNodeList.isEmpty()) {
            return this.visibleByDefault;
        }
		/*
		 *  new logic to deal with the fact that not all children have the same visibility at a level.
		 *  old logic just checked the visibility of item [0] in the list
		 */
		for(TreeNodeMemoryResident theTreeNode : theChildTreeNodeList) {
			if(theTreeNode.isVisible() == true) {
				return true;
			}
		}
        return false;
    }

    @Override
    public synchronized void addBeforeChild(
    		final TreeNodeInfo aParentTreeNodeInfo,
    		final TreeNodeInfo aNewChildTreeNodeInfo,
            final TreeNodeInfo anExistingChildTreeNodeInfo) {
        validateNodeNotInTreeMap(aNewChildTreeNodeInfo);
        final TreeNodeMemoryResident theParentTreeNode = 
        		getNodeFromTreeMapOrRootNode(aParentTreeNodeInfo);
        final boolean theVisibility = getChildrenVisibility(theParentTreeNode);
        if (anExistingChildTreeNodeInfo == null) {
            final TreeNodeMemoryResident theNewChildTreeNode = 
            		theParentTreeNode.addChild(0, aNewChildTreeNodeInfo, theVisibility);
            this.treeNodeMap.put(aNewChildTreeNodeInfo, theNewChildTreeNode);
        } else {
            final int theIndex = 
            		theParentTreeNode.indexOfChildTreeNodeInfoList(anExistingChildTreeNodeInfo);
            final TreeNodeMemoryResident theNewTreeNode = 
            		theParentTreeNode.addChild(
            				theIndex == -1 ? 0 : theIndex, aNewChildTreeNodeInfo, theVisibility );
            this.treeNodeMap.put(aNewChildTreeNodeInfo, theNewTreeNode);
        }
        if (theVisibility) {
            treeStateChanged();
        }
    }

    @Override
    public synchronized void addAfterChild(
    		final TreeNodeInfo aParentTreeNodeInfo,
    		final TreeNodeInfo aNewChildTreeNodeInfo,
            final TreeNodeInfo anExistingChildTreeNodeInfo) {
        validateNodeNotInTreeMap(aNewChildTreeNodeInfo);
        final TreeNodeMemoryResident theParentTreeNode = getNodeFromTreeMapOrRootNode(aParentTreeNodeInfo);
        final boolean theVisibility = getChildrenVisibility(theParentTreeNode);
        if (anExistingChildTreeNodeInfo == null) {
            final TreeNodeMemoryResident theNewTreeNode = theParentTreeNode.addChild(
                    theParentTreeNode.getChildTreeNodeListSize(), aNewChildTreeNodeInfo, aNewChildTreeNodeInfo.isVisible() );
            this.treeNodeMap.put(aNewChildTreeNodeInfo, theNewTreeNode);
        } else {
            final int theTreeNodeIndex = theParentTreeNode.indexOfChildTreeNodeInfoList(anExistingChildTreeNodeInfo);
            final TreeNodeMemoryResident theNewTreeNode = theParentTreeNode.addChild(
                    theTreeNodeIndex == -1 ? theParentTreeNode.getChildTreeNodeListSize() : theTreeNodeIndex + 1,
                    aNewChildTreeNodeInfo,
                    aNewChildTreeNodeInfo.isVisible() );
            this.treeNodeMap.put(aNewChildTreeNodeInfo, theNewTreeNode);
        }
        if (theVisibility) {
            treeStateChanged();
        }
    }

    @Override
    public synchronized void removeNodeRecursively(final TreeNodeInfo aTreeNodeInfo) {
        final TreeNodeMemoryResident theTreeNode = getNodeFromTreeMapOrRootNode(aTreeNodeInfo);
        final boolean isVisibleNodeChanged = removeNodeRecursively(theTreeNode);
        final TreeNodeInfo theParentTreeNodeInfo = theTreeNode.getParentTreeNodeInfo();
        final TreeNodeMemoryResident theParentTreeNode = getNodeFromTreeMapOrRootNode(theParentTreeNodeInfo);
        theParentTreeNode.removeChild(aTreeNodeInfo);
        if (isVisibleNodeChanged) {
            treeStateChanged();
        }
    }

    private boolean removeNodeRecursively(final TreeNodeMemoryResident aTreeNode) {
        boolean isVisibleNodeChanged = false;
        for (final TreeNodeMemoryResident theChildTreeNode : aTreeNode.getChildTreeNodeList()) {
            if (removeNodeRecursively(theChildTreeNode)) {
                isVisibleNodeChanged = true;
            }
        }
        aTreeNode.clearChildren();
        if (aTreeNode.getTreeNodeInfo() != null) {
        	this.treeNodeMap.remove(aTreeNode.getTreeNodeInfo());
            if (aTreeNode.isVisible()) {
                isVisibleNodeChanged = true;
            }
        }
        return isVisibleNodeChanged;
    }

    private void setChildrenVisibility(
    		final TreeNodeMemoryResident aTreeNode,
            final boolean bVisible,
            final boolean bRecursive ) {
        for (final TreeNodeMemoryResident theChildTreeNode : aTreeNode.getChildTreeNodeList()) {
        	theChildTreeNode.setVisible(bVisible == false || this.treeFilter == null ? bVisible
        			: this.treeFilter.showInTree(aTreeNode));
            if (bRecursive) {
                setChildrenVisibility(theChildTreeNode, bVisible, true);
            }
        }
    }
    
//    public void setShowCompletion(TreeNodeInfo aTreeNodeInfo, boolean aBoolean) {
//        final MemoryResidentTreeNode theMemoryResidentTreeNode = getNodeFromTreeMapOrRootNode(aTreeNodeInfo);
//        theMemoryResidentTreeNode.setVisible(false);
//        theMemoryResidentTreeNode.getTreeNodeInfo().setShowCompletion(aBoolean);
//    }

	@Override
	public synchronized void expandDirectChildren(
			final TreeNodeInfo aTreeNodeInfo) {
		Log.d(LOGGING_TAG, "Expanding direct children of " + aTreeNodeInfo);
		final TreeNodeMemoryResident theParentTreeNode = getNodeFromTreeMapOrRootNode(aTreeNodeInfo);
		//setChildrenVisibility(theParentTreeNode, true, false);
		restoreChildrenVisibility(theParentTreeNode);
		theParentTreeNode.getTreeNodeInfo().setExpanded(true); // Hack Alert !!! - SDS
		treeStateChanged();
	}

	private void restoreChildrenVisibility(final TreeNodeMemoryResident aTreeNode) {
		for (final TreeNodeMemoryResident theChildTreeNode : aTreeNode.getChildTreeNodeList()) {
			theChildTreeNode.setVisible(this.treeFilter != null ? this.treeFilter.showInTree(theChildTreeNode) : true);
			if(theChildTreeNode.getTreeNodeInfo().isExpanded()) {
				restoreChildrenVisibility(theChildTreeNode);
			}
		}
	}
	
	@Override
	public void applyTreeFilterCriteria() {
		restoreChildrenVisibility(this.rootTreeNodeMMemoryResident);
	}
    
	@Override
	public void collapseToTreeLevel(int aTreeDepth) {
		int theTreeDepth = aTreeDepth == getMaxTreeDepth() ? 99 : aTreeDepth;
		switch (theTreeDepth) {
			case 1:
				for(TreeNodeInfo theTreeNodeInfoLevel1 : getChildren(this.rootTreeNodeInfo)) {
					collapseChildren(theTreeNodeInfoLevel1);
				}						
				break;
			case 2:
				for(TreeNodeInfo theTreeNodeInfoLevel1 : getChildren(this.rootTreeNodeInfo)) {
					expandDirectChildren(theTreeNodeInfoLevel1);
					for(TreeNodeInfo theTreeNodeInfoLevel2 : getChildren(theTreeNodeInfoLevel1)) {
						collapseChildren(theTreeNodeInfoLevel2);
					}	
				}	
				break;
			case 3:
				for(TreeNodeInfo theTreeNodeInfoLevel1 : getChildren(this.rootTreeNodeInfo)) {
					expandDirectChildren(theTreeNodeInfoLevel1);
					for(TreeNodeInfo theTreeNodeInfoLevel2 : getChildren(theTreeNodeInfoLevel1)) {
						expandDirectChildren(theTreeNodeInfoLevel2);
						for(TreeNodeInfo theTreeNodeInfoLevel3 : getChildren(theTreeNodeInfoLevel1)) {
							collapseChildren(theTreeNodeInfoLevel3);
						}	
					}	
				}	
				break;
			case 4:
				for(TreeNodeInfo theTreeNodeInfoLevel1 : getChildren(this.rootTreeNodeInfo)) {
					expandDirectChildren(theTreeNodeInfoLevel1);
					for(TreeNodeInfo theTreeNodeInfoLevel2 : getChildren(theTreeNodeInfoLevel1)) {
						expandDirectChildren(theTreeNodeInfoLevel2);
						for(TreeNodeInfo theTreeNodeInfoLevel3 : getChildren(theTreeNodeInfoLevel1)) {
							expandDirectChildren(theTreeNodeInfoLevel3);
							for(TreeNodeInfo theTreeNodeInfoLevel4 : getChildren(theTreeNodeInfoLevel1)) {
								collapseChildren(theTreeNodeInfoLevel4);
							}	
						}	
					}	
				}	
				break;
			case 99:
				for(TreeNodeInfo theTreeNodeInfoLevel1 : getChildren(this.rootTreeNodeInfo)) {
					expandEverythingBelow(theTreeNodeInfoLevel1);
				}	
				break;
			default:
		}
		treeStateChanged();
	}

    private int getMaxTreeDepth() {
		// TODO Auto-generated method stub
		return 0;
	}

	@Override
    public synchronized void expandEverythingBelow(final TreeNodeInfo aTreeNodeInfo) {
        Log.d(LOGGING_TAG, "Expanding all children below " + aTreeNodeInfo);
        final TreeNodeMemoryResident theParentTreeNode = getNodeFromTreeMapOrRootNode(aTreeNodeInfo);
        setChildrenVisibility(theParentTreeNode, true, true);
        treeStateChanged();
    }

    @Override
    public synchronized void collapseChildren(final TreeNodeInfo aTreeNodeInfo) {
        final TreeNodeMemoryResident theParentTreeNode = getNodeFromTreeMapOrRootNode(aTreeNodeInfo);
        if (theParentTreeNode == this.rootTreeNodeMMemoryResident) {
            for (final TreeNodeMemoryResident theChildTreeNode : this.rootTreeNodeMMemoryResident.getChildTreeNodeList()) {
                setChildrenVisibility(theChildTreeNode, false, true);
            }
        } else {
            setChildrenVisibility(theParentTreeNode, false, true);
        }
        theParentTreeNode.getTreeNodeInfo().setExpanded(false);  // Hack Alert  -  SDS
        treeStateChanged();
    }

    @Override
    public synchronized TreeNodeInfo getNextSibling(final TreeNodeInfo aTreeNodeInfo) {
        final TreeNodeInfo theParentTreeNodeInfo = getParent(aTreeNodeInfo);
        final TreeNodeMemoryResident theParentTreeNode = getNodeFromTreeMapOrRootNode(theParentTreeNodeInfo);
        boolean isReturnNextNode = false;
        for (final TreeNodeMemoryResident theChildTreeNode : theParentTreeNode.getChildTreeNodeList()) {
            if (isReturnNextNode) {
                return theChildTreeNode.getTreeNodeInfo();
            }
            if (theChildTreeNode.getTreeNodeInfo().equals(aTreeNodeInfo)) {
                isReturnNextNode = true;
            }
        }
        return null;
    }

    @Override
    public synchronized TreeNodeInfo getPreviousSibling(final TreeNodeInfo aTreeNodeInfo) {
        final TreeNodeInfo theParentTreeNodeInfo = getParent(aTreeNodeInfo);
        final TreeNodeMemoryResident theParentTreeNode = getNodeFromTreeMapOrRootNode(theParentTreeNodeInfo);
        TreeNodeInfo thePreviousSiblingTreeNodeInfo = null;
        for (final TreeNodeMemoryResident theChildTreeNode : theParentTreeNode.getChildTreeNodeList()) {
            if (theChildTreeNode.getTreeNodeInfo().equals(aTreeNodeInfo)) {
                return thePreviousSiblingTreeNodeInfo;
            }
            thePreviousSiblingTreeNodeInfo = theChildTreeNode.getTreeNodeInfo();
        }
        return null;
    }

    @Override
    public synchronized boolean isInTree(final TreeNodeInfo aTreeNodeInfo) {
        return this.treeNodeMap.containsKey(aTreeNodeInfo);
    }

    @Override
    public synchronized int getVisibleCount() {
        return getVisibleTreeNodeInfoList().size();
    }

	@Override
	public synchronized List<TreeNodeInfo> getVisibleTreeNodeInfoList()
	{
		if (this.visibleTreeNodeInfoList == null)
		{
			initializeVisibleTreeNodeInfoList();
		}
		if (this.unmodifiableVisibleTreeNodeInfoList == null)
		{
			this.unmodifiableVisibleTreeNodeInfoList = Collections.unmodifiableList(this.visibleTreeNodeInfoList);
		}
		return this.unmodifiableVisibleTreeNodeInfoList;
	}

	private void initializeVisibleTreeNodeInfoList()
	{
		this.visibleTreeNodeInfoList = new ArrayList<TreeNodeInfo>();
		LinkedList<TreeNodeMemoryResident> theQueue = new LinkedList<TreeNodeMemoryResident>();
		theQueue.push(this.rootTreeNodeMMemoryResident);
		while (!theQueue.isEmpty())
		{
			TreeNodeMemoryResident theNode = theQueue.pop();
			if (theNode.getLevel() == -1)
			{
				theQueue.addAll(0, theNode.getChildTreeNodeList());
			}
			else
			{
				if (theNode.isVisible())
				{
					this.visibleTreeNodeInfoList.add(theNode.getTreeNodeInfo());
					theQueue.addAll(0, theNode.getChildTreeNodeList());
				}
			}
		}
	}

    @Override
    public synchronized void registerDataSetObserver(
            final DataSetObserver aTreeObserver) {
    	this.treeStateObservers.add(aTreeObserver);
    }

    @Override
    public synchronized void unregisterDataSetObserver(
            final DataSetObserver aTreeObserver) {
    	this.treeStateObservers.remove(aTreeObserver);
    }

    @Override
    public int getLevel(final TreeNodeInfo aTreeNodeInfo) {
        return getNodeFromTreeMapOrThrow(aTreeNodeInfo).getLevel();
    }

    @Override
    public Integer[] getHierarchyDescription(final TreeNodeInfo aTreeNodeInfo) {
        final int theLevel = getLevel(aTreeNodeInfo);
        final Integer[] theHierarchyArray = new Integer[theLevel + 1];
        int theCurrentLevel = theLevel;
        TreeNodeInfo theCurrentTreeNodeInfo = aTreeNodeInfo;
        TreeNodeInfo theParentTreeNodeInfo = getParent(theCurrentTreeNodeInfo);
        while (theCurrentLevel >= 0) {
            theHierarchyArray[theCurrentLevel--] = getChildren(theParentTreeNodeInfo).indexOf(theCurrentTreeNodeInfo);
            theCurrentTreeNodeInfo = theParentTreeNodeInfo;
            theParentTreeNodeInfo = getParent(theParentTreeNodeInfo);
        }
        return theHierarchyArray;
    }

//    private void appendToStringBuilder(final StringBuilder aStringBuilder, final TreeNodeInfo aNodeObject) {
//        if (aNodeObject != null) {
//            final TreeNodeInfo theTreeNode = getTreeNodeInfo(aNodeObject);
//            final int theIndent = theTreeNode.getLevel() * 4;
//            final char[] indentString = new char[theIndent];
//            Arrays.fill(indentString, ' ');
//            aStringBuilder.append(indentString);
//            aStringBuilder.append(theTreeNode.toString());
//            aStringBuilder.append(Arrays.asList(getHierarchyDescription(aNodeObject)).toString());
//            aStringBuilder.append("\n");
//        }
//        final List children = getChildren(aNodeObject);
//        for (final TreeNodeInfo child : children) {
//            appendToStringBuilder(aStringBuilder, child);
//        }
//    }
//
//    @Override
//    public synchronized String toString() {
//        final StringBuilder theStringBuilder = new StringBuilder();
//        appendToStringBuilder(theStringBuilder, null);
//        return theStringBuilder.toString();
//    }

    @Override
    public synchronized void clearTree() {
    	this.treeNodeMap.clear();
    	this.rootTreeNodeMMemoryResident.clearChildren();
        treeStateChanged();
    }

    @Override
    public void refreshViews() {
        treeStateChanged();
    }
    
    @Override
    public boolean showInTree(TreeNodeInfo aTreeNodeInfo) {
    	TreeNodeMemoryResident theTreeNodeMemoryResident = getNodeFromTreeMapOrThrow(aTreeNodeInfo);
    	return this.treeFilter.showInTree(theTreeNodeMemoryResident);
    }

}
